<template>
  <view class="time-picker">
    <picker-view
      class="picker-view"
      mask-class="picker-mask"
      indicator-class="picker-indicator"
      :value="[hourIndex]"
      :style="pickerViewStyle"
      @change="onHourChange"
    >
      <picker-view-column class="picker-column align-left">
        <view
          class="picker-item"
          v-for="(hour, index) in pickerHours"
          :key="index"
          :class="{ active: hourIndex === index }"
          :style="{ ...itemStyle, ...(hourIndex === index && itemActiveStyle) }"
        >
          <view class="text">{{ hour.label }}</view>
        </view>
      </picker-view-column>
    </picker-view>

    <view class="picker-divider"></view>

    <picker-view
      class="picker-view"
      mask-class="picker-mask"
      indicator-class="picker-indicator"
      :value="[minuteIndex]"
      :style="pickerViewStyle"
      @change="onMinuteChange"
    >
      <picker-view-column class="picker-column align-right">
        <view
          class="picker-item"
          v-for="(minute, index) in pickerMinutes"
          :key="index"
          :class="{ active: minuteIndex === index }"
          :style="{ ...itemStyle, ...(minuteIndex === index && itemActiveStyle) }"
        >
          <view class="text">{{ minute.label }}</view>
        </view>
      </picker-view-column>
    </picker-view>
  </view>
</template>

<script setup lang="ts">
import { nextTickPromise } from '@/utils/mpHelper'
import dayjs, { Dayjs } from 'dayjs'
import { debounce } from 'lodash'
import type { CSSProperties } from 'vue'

export type OrderTime = {
  label: string // 字符格式
  value: number // 数字格式
}
export type OrderHour = OrderTime & {
  addDays: number // 往后追加的天数 (默认0天)
  beginMinute?: number // 起始分钟 (默认0分)
  endMinute?: number // 截至分钟 (默认59分)
}
export type ChangePayload = {
  text: string
  timestamp: number
}

const props = withDefaults(
  defineProps<{
    value: number // timestamp
    start: number // timestamp
    end: number // timestamp
    interval: number // 单位 秒
    pickerStyle?: CSSProperties
    itemStyle?: CSSProperties
    itemActiveStyle?: CSSProperties
  }>(),
  {
    interval: 600, // 10min === 600sec
  }
)
const emit = defineEmits<{
  (e: 'change', payload: ChangePayload): void
}>()

// 样式
const pickerViewStyle = computed<CSSProperties>(() => ({
  height: '226rpx',
  ...props.pickerStyle,
}))

// 组件优化 非挂载状态不处理时间
let isMounted = false
onBeforeMount(() => {
  isMounted = true
  initOrderTime()
})
onBeforeUnmount(() => {
  isMounted = false
})

// 时间
const pickerHours = ref<OrderHour[]>([])
const pickerMinutes = ref<OrderTime[]>([])
const hourIndex = ref(0)
const minuteIndex = ref(0)
async function initOrderTime() {
  if (!isMounted) return

  const currentTime = dayjs(props.value)

  /**
   * 重置下标
   */
  hourIndex.value = 0
  minuteIndex.value = 0

  /**
   * 重置小时
   */
  pickerHours.value = getHourList()
  const needUpdateHourIndex = getMostMatchedIndex(pickerHours.value, currentTime.hour())

  /**
   * 重置分钟
   */
  pickerMinutes.value = getMinuteList(needUpdateHourIndex)
  const needUpdateMinuteIndex = getMostMatchedIndex(pickerMinutes.value, currentTime.minute())

  /**
   * 回填下标
   */
  await nextTickPromise()
  hourIndex.value = needUpdateHourIndex
  minuteIndex.value = needUpdateMinuteIndex
}
watch(
  () => [props.start, props.end],
  (n, o) => {
    initOrderTime()
  }
)
watch(
  () => props.value,
  () => {
    if (changeTriggeredBySelf) {
      changeTriggeredBySelf = false
      return
    }
    initOrderTime()
  }
)

let changeMinuteByHour = false
async function onHourChange(e: WechatMiniprogram.PickerChange) {
  let [newHourIndex] = e.detail.value as [number]

  const oldHourItem = pickerHours.value[hourIndex.value]
  const oldMinuteIndex = minuteIndex.value
  const oldMinuteItem = pickerMinutes.value[minuteIndex.value]
  const newHourItem = pickerHours.value[newHourIndex]

  // 修改小时下标
  hourIndex.value = newHourIndex

  // 选择时间时 判断时间是否存在限制 存在限制则计算新的下标
  if (oldHourItem.beginMinute || oldHourItem.endMinute || newHourItem?.beginMinute || newHourItem?.endMinute) {
    // 记录旧分钟
    const oldMinuteValue = oldMinuteItem.value
    /**
     * 重置分钟
     */
    pickerMinutes.value = getMinuteList()

    const minuteList = pickerMinutes.value
    const newMinuteListMaxIndex = minuteList.length - 1
    let overwriteMinuteIndex = minuteList.findIndex((minute) => minute.value === oldMinuteValue)
    if (overwriteMinuteIndex === -1) {
      const newLastMinuteValue = minuteList[newMinuteListMaxIndex].value
      overwriteMinuteIndex = oldMinuteValue > newLastMinuteValue ? newMinuteListMaxIndex : 0
    }

    // 分钟下标超出最大下标时 wx会触发分钟change事件 需要标记忽略分钟的change事件
    changeMinuteByHour = oldMinuteIndex > newMinuteListMaxIndex

    await nextTickPromise()
    // 修改分钟下标
    minuteIndex.value = overwriteMinuteIndex
  }

  triggerTimeChange()
}

async function onMinuteChange(e: WechatMiniprogram.PickerChange) {
  if (changeMinuteByHour) {
    changeMinuteByHour = false
    return
  }

  let [newMinuteIndex] = e.detail.value as [number]

  // 修改分钟下标
  minuteIndex.value = newMinuteIndex

  triggerTimeChange()
}

/**
 * 根据旧值与新选项 找出匹配的下标
 */
function getMostMatchedIndex(newOptions: OrderTime[], oldValue: number): number {
  const newOptionIndex = newOptions.findIndex((option) => option.value === oldValue)
  // 防止出现找不到值 index为-1的情况 为-1时取0
  return Math.max(newOptionIndex, 0)
}

// 当前事件由组件本身触发 标志位
let changeTriggeredBySelf = false
const triggerTimeChange = debounce(async () => {
  await nextTickPromise()

  const hourItem = pickerHours.value[hourIndex.value]
  const minuteItem = pickerMinutes.value[minuteIndex.value]
  if (!hourItem || !minuteItem) return

  const timeText = `${hourItem.label}:${minuteItem.label}`
  const date = dayjs(props.start).add(hourItem.addDays, 'days').hour(hourItem.value).minute(minuteItem.value).toDate()
  emit('change', { text: timeText, timestamp: date.getTime() })
  changeTriggeredBySelf = true
}, 10)

function getHourList(): OrderHour[] {
  const hourList: OrderHour[] = []

  const startDateTime = dayjs(props.start)
  const endDateTime = dayjs(props.end)

  const startDate = _getDateWithoutTime(startDateTime)
  const startTimestamp = props.start
  const endTimestamp = props.end
  // const interval = props.interval * 1000 // 转为毫秒
  const increment = 3600000 // 转为毫秒

  for (let time = startTimestamp; time <= endTimestamp; time += increment) {
    const date = dayjs(time)
    const hour = date.hour()
    const hourText = String(hour).padStart(2, '0')
    const currentDate = _getDateWithoutTime(date)
    const addDays = currentDate.diff(startDate, 'days')

    const hourItem: OrderHour = {
      label: hourText,
      value: hour,
      addDays,
    }
    // 第一项
    if (time === startTimestamp) {
      hourItem.beginMinute = startDateTime.minute()
    }
    // 最后一项
    if (time + increment > endTimestamp) {
      hourItem.endMinute = endDateTime.minute()
    }
    hourList.push(hourItem)
  }
  return hourList
}

function getMinuteList(selectedHourIndex: number = hourIndex.value): OrderTime[] {
  const currentHour = pickerHours.value[selectedHourIndex]
  const beginMinute = currentHour?.beginMinute || 0
  const endMinute = currentHour?.endMinute || 59
  const minuteList: OrderTime[] = []
  const interval = Math.max(parseInt((props.interval / 60).toFixed(0)), 1) // 间隔 (最少1分钟)
  for (let minute = beginMinute; minute <= endMinute; minute += interval) {
    const minuteText = String(minute).padStart(2, '0')
    minuteList.push({
      label: minuteText,
      value: minute,
    })
  }
  return minuteList
}

function _getDateWithoutTime(date: string | number | Dayjs | Date | null | undefined) {
  return dayjs(date).hour(0).minute(0).second(0).millisecond(0)
}
</script>

<style lang="less">
.time-picker {
  position: relative;
  width: 408rpx;
  color: rgba(var(--dark-vector), 0.8);
  display: flex;
  align-items: center;

  /**
   * 去除默认样式
   */
  .picker-mask {
    background: transparent;
    border: none;
  }
  .picker-indicator {
    height: 72rpx;
    &::before,
    &::after {
      border: none;
    }
  }

  .picker-view {
    width: 204rpx;
  }

  .picker-item {
    display: flex;
    align-items: center;
    font-weight: 400;
    font-size: 36rpx;
    line-height: 50rpx;
    transition: all 0.2s ease;
    &.active {
      font-weight: 600;
      font-size: 48rpx;
      line-height: 68rpx;
      color: var(--primary-color);
    }
    > .text {
      width: 140rpx;
      text-align: center;
    }
  }
  .picker-column {
    &.align-left .picker-item {
      justify-content: flex-start;
    }
    &.align-right .picker-item {
      justify-content: flex-end;
    }
  }

  /**
  * 分隔指示点
  */
  .picker-divider {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    display: flex;
    justify-content: center;
    align-items: center;
    flex-flow: column nowrap;
    --size: 10rpx;
    &::before,
    &::after {
      display: block;
      content: '';
      width: var(--size);
      height: var(--size);
      background-color: var(--primary-color);
      border-radius: 50%;
    }
    &::after {
      margin-top: var(--size);
    }
  }
}
</style>
